#include <stdio.h>
#include <iostream>
#include <fstream>
#include <log.hpp>
#include <memfs.h>
#include <timer.h>
#include <getopt.h>

#include "sc.h"
#include "globals.h"
#include "comms.h"
#include "settings.h"
#include "scu.h"
#include "scc.h"

#define SC_MAX_PROCESSORS 8

extern "C" scu_list_t scus_procs, scus_streams, scus_bufs, scus_ops;

static CLog clg;

static Mfs ops_fs;
static Mshell ops(ops_fs);

void *scbBenchProcessor(void *proc);
void scbResultCallback(scc_hdr_t *hdr);
void scbRead(char *file);
void scbAwaitOperation(sc_op_t *op);
char *scbGetOperationName(int opid);
char *scbGetArgumentName(int opid, int argid);

extern scc_proc_t scc_master;

extern "C" int MAIN(int argc, char **argv)
{
  int id;
  sc_proc_t *p;
  pthread_t pthreads[SC_MAX_PROCESSORS];

  clg.notice.Edit(LOG_GROUP_SET_LEVEL);

  logRegisterThread("main");

  scInit(argc, argv);

  scc_master.controlcallback = scbResultCallback;

  abortOnNULL(STREAM(clg.err.GetStream()), settings.intracefile);
  abortOnNULL(STREAM(clg.err.GetStream()), settings.outbenchfile);

  /* Read trace XML file */
  scuStatRead(settings.intracefile);

  /* Start benchmarking */
  id = -1;
  while (tblget(&sc_procs, id = tblgetnexti(&sc_procs, id), &p))
    pthread_create(&pthreads[id], NULL, scbBenchProcessor, (void*)p);

  /* Wait for it to finish */
  id = -1;
  while (tblget(&sc_procs, id = tblgetnexti(&sc_procs, id), &p))
    pthread_join(pthreads[id], NULL);

  /* Average the trials */
  char *element, *element2;

  abortOnError(STREAM(clg.err.GetStream()), ops.FindInitDirectoryElements("operations"));
  while (!ops.FindGetNextElement(&element))
  {
    Mshell opshell(ops, element);

    abortOnError(STREAM(clg.err.GetStream()), opshell.FindInitDirectoryElements("targets"));
    while (!opshell.FindGetNextElement(&element2))
    {
      unsigned long long totaltime = 0;
      int trials                   = 0;
      Mshell tshell(opshell, element2);
      
      if (!tshell.GetContent("trials", &trials) &&
          !tshell.GetContent("time", &totaltime))
      {
        clg.info() << HDR << "Averaging operation " << element << " target " << element2 << " over " << trials << " trials" << std::endl;
 
        tshell.MkEntry("time", totaltime/(1000000.0*trials));
        tshell.RmEntry("trials");
      }
    }
  }

  /* Write operation structure to file */
  std::ofstream ofs(settings.outbenchfile);
  XMLParser xmlout(ofs);
  ops.Export(xmlout, "/");

  scExit();

  return 0;
}

void *scbBenchProcessor(void *proc)
{
  int id;
  char dbuf[256];
  scu_listitem_t *item, *item2;
  sc_proc_t *p = (sc_proc_t *) proc;
  scs_target_t *sp;
  sc_op_t o, argop;
  sc_arg_t *a;
  scus_op_t *so;
  scus_arg_t *sa;
  sc_var_t *v;
  sc_instance_t *i;
  sc_buf_t *b;
  sc_var_t argvnum, argvbuf;
  table_t vars;
  Mshell myops(ops, "/");

  logRegisterThread(p->name);

  abortOnNULL(STREAM(clg.err.GetStream()), tblget(&scs_targets, p->id, &sp));

  p->bufsize = 65536;

  o.id = 0;
  o.processor = p;

  /* Initialize buffers */
  abortOnNegative(STREAM(clg.err.GetStream()), tblcreate(&vars, sizeof(sc_var_t)));
  for (int ii=0; ii < SC_MAX_ARGUMENTS; ii++)
  {
    abortOnNegative(STREAM(clg.err.GetStream()), tbladdnew(&vars, &v));
    v->id = ii;

    abortOnNegative(STREAM(clg.err.GetStream()), tblcreate(&v->instances, sizeof(sc_instance_t)));
    abortOnNegative(STREAM(clg.err.GetStream()), tbladdnew(&v->instances, &i));
    i->id = 0;
    i->variable = v;

    abortOnNegative(STREAM(clg.err.GetStream()), tblcreate(&v->buffers, sizeof(sc_buf_t)));
    abortOnNegative(STREAM(clg.err.GetStream()), tbladdnewi(&v->buffers, &b, p->id));
    b->id = ii;
    b->processor = p;
    b->stream = v;
  }

  /* Initialize load operation */
  argop.id = 255;
  argop.processor = p;
  argop.type = LOAD;
  abortOnNegative(STREAM(clg.err.GetStream()), tblcreate(&argop.arguments, sizeof(sc_arg_t)));

  abortOnNegative(STREAM(clg.err.GetStream()), tbladdnew(&argop.arguments, &a));
  a->id = 0;
  a->intent = SC_INTENT_IN;
  a->operation = &argop;
  argvnum.id = 253;
  argvnum.type = SC_VAR_INTEGER;
  argvnum.size = sizeof(int);
  abortOnNegative(STREAM(clg.err.GetStream()),  tblcreate(&argvnum.instances, sizeof(sc_instance_t)));
  abortOnNegative(STREAM(clg.err.GetStream()), tbladdnew(&argvnum.instances, &i));
  i->id = 0;
  i->variable = &argvnum;
  a->instance = i;

  abortOnNegative(STREAM(clg.err.GetStream()), tbladdnew(&argop.arguments, &a));
  a->id = 1;
  a->intent = SC_INTENT_OUT;
  a->operation = &argop;
  argvbuf.id = 254;
  argvbuf.type = SC_VAR_STREAM;
  argvnum.size = sizeof(int);
  abortOnNegative(STREAM(clg.err.GetStream()), tblcreate(&argvbuf.buffers, sizeof(sc_buf_t)));
  abortOnNegative(STREAM(clg.err.GetStream()), tbladdnewi(&argvbuf.buffers, &b, p->id));
  b->processor = p;
  abortOnNegative(STREAM(clg.err.GetStream()), tblcreate(&argvbuf.instances, sizeof(sc_instance_t)));
  abortOnNegative(STREAM(clg.err.GetStream()), tbladdnew(&argvbuf.instances, &i));
  i->id = 0;
  i->variable = &argvbuf;
  a->instance = i;

  /* Go through all operations */  
  item = NULL;
  while ((so=(scus_op_t*)scuListEnumerateItems(&scus_ops, &item)))
  {
    /* See if we can run this operation */
    if (!tblget(&sp->operations, so->type, NULL))
      continue;

    /* See if we've benchmarked this operation already */
    sprintf(dbuf, "/operations/%s/targets/%s", scbGetOperationName(so->type),  p->name);
    if (myops.ChDir(dbuf))
    {
      myops.MkDir(dbuf);
      myops.ChDir(dbuf);
    }

    {
      unsigned long long totaltime = 0, trialtime = 999999999;
      int trials    = 0;
      int ii;
      
      o.type = so->type;

      for (ii=0; ii != 10; ++ii)
      {
	/* Set up arguments */
	abortOnNegative(STREAM(clg.err.GetStream()), tblcreate(&o.arguments, sizeof(sc_arg_t)));
	item2 = NULL;
	while ((sa = (scus_arg_t*)scuListEnumerateItems(&so->args, &item2)))
	{
	  abortOnNegative(STREAM(clg.err.GetStream()), tbladdnewi(&o.arguments, &a, sa->id));
	  a->id = sa->id;
	  a->operation = &o;
	  abortOnNULL(STREAM(clg.err.GetStream()),tblget(&vars, sa->id, &v));
	
	  abortOnNULL(STREAM(clg.err.GetStream()),tblget(&v->instances, 0, &a->instance));
	  a->instance->variable->type = sa->type;
	  a->intent = sa->intent;	
	
	  switch (sa->type)
	  {
	    case SC_VAR_STREAM:
	      /* Allocate buffer with enough memory */
	      abortOnNULL(STREAM(clg.err.GetStream()), tblget(&v->buffers, p->id, &b));
	      b->size = MAX(1024*1024, sa->i->stream->bytes);
	      clg.notice() << HDR << "Setting up buffer argument " << sa->id
			   << std::endl;
	      scComRemoteBufferSetup(b);
	    
	      if (sa->intent == SC_INTENT_IN)
	      {
		/* Create an operation filling the buffer from
		   intermediate storage */
		clg.notice() << HDR << "Loading input buffer argument " << sa->id
			     << " from " << sa->op->proc->name << "."
			     << sa->i->counter << std::endl;

		argvnum.data = &sa->i->counter;
		abortOnNULL(STREAM(clg.err.GetStream()), tblget(&argvbuf.buffers, p->id, &b));
		b->id = sa->id;
	      
		p->bufsize = 0;
		scComRemoteOperationEnqueue(&argop);	      
		id = -1;
		while (tblget(&argop.arguments, id = tblgetnexti(&argop.arguments, id), &a))
		  scComRemoteArgumentDistribute(a);
		scbAwaitOperation(&argop);
	      }
	      break;
	    case SC_VAR_INTEGER:
	      a->instance->variable->data = &sa->d;
	      a->instance->variable->size = sizeof(int);
	      break;
	    case SC_VAR_DOUBLE:
	      a->instance->variable->data = &sa->f;
	      a->instance->variable->size = sizeof(double);
	      break;
	  }
	}
      
	/* Start operation */
	clg.notice() << HDR << "Running operation type " << o.type
		     << std::endl;
      
	p->bufsize = 0;
	scComRemoteOperationEnqueue(&o);
	id = -1;
	while (tblget(&o.arguments, id = tblgetnexti(&o.arguments, id), &a))
	  scComRemoteArgumentDistribute(a);

	/* Time it */      
	scbAwaitOperation(&o);
      
	trialtime = MIN(trialtime, p->bufsize);
	clg.notice() << HDR << "Operation took " << p->bufsize << " us" << std::endl;

	abortOnNegative(STREAM(clg.err.GetStream()),  tbldestroy(&o.arguments, 1));

	item2 = NULL;
	while ((sa = (scus_arg_t*)scuListEnumerateItems(&so->args, &item2)))
	{
	  if (sa->type == SC_VAR_STREAM)
	  {
            unsigned int prevlength=0;

	    /* Write to data structure */
	    sprintf(dbuf, "/operations/%s/arguments/%s/length", scbGetOperationName(so->type),
		    scbGetArgumentName(so->type, sa->id));

            ops.GetContent(dbuf, &prevlength);
	    ops.MkEntry(dbuf, MAX(sa->i->stream->bytes, prevlength));
	    
	    /* Finalize buffers */
	    abortOnNULL(STREAM(clg.err.GetStream()), tblget(&vars, sa->id, &v));
	    abortOnNULL(STREAM(clg.err.GetStream()), tblget(&v->buffers, p->id, &b));
	    scComRemoteBufferFinalize(b);
	  }
	}    
      }

      clg.notice() << HDR << "Minimum time was " << trialtime << " us" << std::endl;

      /* Write to data structure */
      myops.GetContent("trials", &trials);
      myops.MkEntry("trials", trials + 1);
      myops.GetContent("time", &totaltime);
      myops.MkEntry("time", totaltime + trialtime);
    }
  }

  return NULL;
}

void scbResultCallback(scc_hdr_t *hdr)
{
  sc_proc_t *p;
  scc_msg_t *msg=(scc_msg_t*)hdr;

  if (msg->header.type == SCC_MSGTYPE_OPERATIONCOMPLETED)
  {
    abortOnNULL(STREAM(clg.err.GetStream()), tblget(&sc_procs, msg->header.source, &p));
    p->bufsize = *((unsigned long long*)msg->data)+1;
  }
}

void scbAwaitOperation(sc_op_t *op)
{
  while (op->processor->bufsize == 0)
    sched_yield();
}

char *scbGetOperationName(int opid)
{
  scs_operation_t *o;

  if (tblget(&scs_operations, opid, &o))
    return o->name;

  abortErrorl((STREAM(clg.err.GetStream()), "Couldn't find operation type %d\n", opid));
  return NULL;
}

char *scbGetArgumentName(int opid, int argid)
{
  scs_operation_t *o;
  scs_operation_argument_t *a;

  if (tblget(&scs_operations, opid, &o))
  {
    if (tblget(&o->arguments, argid, &a))
      return a->name;
    
    /* Hmm, argument name isn't known... */
    Mshell myops(ops, "/");

    char *name = new char[5];
    sprintf(name, "id%d", argid);

    char dbuf[PATH_MAX];
    sprintf(dbuf, "/operations/%s/arguments/%s/id", o->name, name);
    myops.MkEntry(dbuf, argid);

    return name;
  }

  abortErrorl((STREAM(clg.err.GetStream()), "Couldn't find operation type %d\n", opid));
  return NULL;
}
